<?php
/*

Copyright (c) 2010 four.zero.one.unauthorized@gmail.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Author: four.zero.one.unauthorized@gmail.com
Initial Build Date: 7/28/2010
Rev Date: 7/28/2010 13:00
Summary: PHP Registry Editor Singleton Class (rough draft)

Extensions:
1) MB

Dependancies: 
1) COM - winmgmts:StdRegProv
2) WIN VISTA
3) attached variant_lib.php CSTM_variant_get_php_value() function
4) PHP: 5.3.3

References:
1) http://code.google.com/p/corporate-overload/wiki/registry_editor
2) http://support.microsoft.com/kb/310516/
3) http://msdn.microsoft.com/en-us/library/aa393664%28v=VS.85%29.aspx
4) http://support.microsoft.com/kb/256986

*/

require_once 'variant_lib.php';

abstract class Reg_Editor {
	private static $Com;
	private static $reg_rollback_file = '\registry_editor_rollback.reg';
	private static $winmgmts_com_statement = 'winmgmts:\\root\default:StdRegProv';
	const HKEY_CLASSES_ROOT = 0x80000000;
	const HKEY_CURRENT_USER = 0x80000001;
	const HKEY_LOCAL_MACHINE = 0x80000002;
	const HKEY_USERS = 0x80000003;
	const HKEY_CURRENT_CONFIG = 0x80000005;
	const HKEY_DYN_DATA = 0x80000006;
	const REG_SZ = 1;
	const REG_EXPAND_SZ = 2;
	const REG_BINARY = 3;
	const REG_DWORD = 4;
	const REG_QWORD = 11;
	const REG_MULTI_SZ = 7;
	const RULE_INVALID_CHARACTERS = 0;
	const RULE_VALUE_DOES_NOT_EXIST = 1;
	const RULE_ROOT_UNKNOWN = 2;
	const RULE_COM_ERROR = 3;
	const RULE_ACCESS_DENIED = 4;
	const RULE_VALUE_TYPE_UNKNOWN = 5;
	const RULE_VALUE_TYPE_CHANGE = 6;
	const RULE_KEY_OVERWRITE = 7;
	const RULE_VALUE_OVERWRITE = 8;
	const RULE_KEY_DOES_NOT_EXIST = 9;
	const RULE_OPERATION_FAILED = 10;
	const RULE_VALUE_INVALID = 11;
	
	static function value_check($level, $root, $key_path, $value_name, $value_type=null, $value=null) {
		if (!(is_string($value_name) AND ctype_graph($value_name) AND ctype_print($value_name))) {
			throw new Exception("\$value_name is not a string or contains control or space characters!", self::RULE_INVALID_CHARACTERS);
			}
		self::key_check($level, $root, $key_path);
		$key_list = self::EnumValues($root, $key_path);
		if (!array_key_exists($value_name, $key_list)) {
			throw new Exception("$value_name does not exist!", self::RULE_VALUE_DOES_NOT_EXIST);
			}
		return (int) $key_list[$value_name];
		}
	
	static function key_check($level, $root, $key_path) {
		if (!self::$Com) {
			self::$Com = new COM(self::$winmgmts_com_statement);
			}
		if (!(is_string($root) AND ctype_graph($root) AND ctype_print($root))) {
			throw new Exception("\$root is not a string or contains control or space characters!", self::RULE_INVALID_CHARACTERS);
			}
		if (!defined("self::$root") OR substr($root, 0, 5) !== "HKEY_") {
			throw new Exception("Root unknown!", self::RULE_ROOT_UNKNOWN);
			}
		if (!(is_string($key_path) AND ctype_graph($key_path) AND ctype_print($key_path))) {
			throw new Exception("\$key_path is not a string or contains control or space characters!", self::RULE_INVALID_CHARACTERS);
			}
		if (!preg_match("/.*/i", $key_path)) {
			throw new Exception("\$key_path $key_path contains invalid characters!", self::RULE_INVALID_CHARACTERS);
			}
		$level = (int) $level;
		$has_access = new VARIANT();
		$access_level = new VARIANT((($level) ? 9 : 65574));
		$success = (int) self::$Com->CheckAccess(constant("self::$root"), $key_path, $access_level, $has_access);
		if ($success === 2) {
			throw new Exception("Key not found!", self::RULE_KEY_DOES_NOT_EXIST);
			}
		else if ($success > 0) {
			throw new Exception("Com object failed.  Error code $success returned!", self::RULE_OPERATION_FAILED);
			}
		if (!(bool) $has_access) {
			throw new Exception("Access denied!", self::RULE_ACCESS_DENIED);
			}
		return true;
		}

	static function GetValue($root, $key_path, $value_name) {
		$value_type = self::value_check(0, $root, $key_path, $value_name);
		$func_array = array(
			self::REG_SZ => 'GetStringValue',
			self::REG_EXPAND_SZ => 'GetExpandedStringValue',
			self::REG_BINARY => 'GetBinaryValue',
			self::REG_DWORD => 'GetDWORDValue',
			self::REG_QWORD => 'GetQWORDValue',
			self::REG_MULTI_SZ => 'GetMultiStringValue'
			);
		if (!array_key_exists($value_type, $func_array)) {
			throw new Exception("Type $value_type unknown!", self::RULE_VALUE_TYPE_UNKNOWN);
			}
		$object_data = new VARIANT();
		$success = self::$Com->$func_array[$value_type](constant("self::$root"), $key_path, $value_name, $object_data);
		if ($success > 0) {
			throw new Exception("Cannot get value.  Error code $success returned!", self::RULE_OPERATION_FAILED);
			}
		return CSTM_variant_get_php_value($object_data);
		}

	static function SetValue($root, $key_path, $value_name, $value_type=0, $value=null) {
		$previous_type = null;
		try {
			$previous_type = self::value_check(1, $root, $key_path, $value_name, $value_type);
			if ($previous_type !== $value_type) {
				throw new Exception("Cannot update value.  Type specified is different!", self::RULE_VALUE_TYPE_CHANGE);
				}
			$previous_value = self::GetValue($root, $key_path, $value_name);
			}
		catch (Exception $e) {
			if ($e->getCode() !== self::RULE_VALUE_DOES_NOT_EXIST) {throw $e;}
			}
		$func_array = array(
			self::REG_SZ => 'SetStringValue',
			self::REG_EXPAND_SZ => 'SetExpandedStringValue',
			self::REG_BINARY => 'SetBinaryValue',
			self::REG_DWORD => 'SetDWORDValue',
			self::REG_QWORD => 'SetQWORDValue',
			self::REG_MULTI_SZ => 'SetMultiStringValue'
			);
		if (!array_key_exists($value_type, $func_array)) {
			throw new Exception("Type $value_type unknown!", self::RULE_VALUE_TYPE_UNKNOWN);
			}
		if ($value !== null) {
			switch ((int) $value_type) {
				case self::REG_SZ:
				case self::REG_EXPAND_SZ:
					if (!is_string($value)) {
						throw new Exception("Value is not a string!", self::RULE_VALUE_INVALID);
						}
					break;
				case self::REG_BINARY:
					if (!is_array($value)) {
						throw new Exception("Value is not an array of bytes!", self::RULE_VALUE_INVALID);
						}
					$thisclass = __CLASS__;
					array_walk($value, function($val, $key) use ($thisclass) {if (!is_int($val) OR (int) $val < 0 OR (int) $val > 255) {
						throw new Exception("Array value is not an int!", $thisclass::RULE_VALUE_INVALID);
						}});
					break;
				case self::REG_DWORD:
					if (!is_int($value) OR bccomp("4294967296", (string) $value) !== 1 OR bccomp((string) $value, "0") !== 1) {
						throw new Exception("Value is not an int!", self::RULE_VALUE_INVALID);
						}
					break;
				case self::REG_QWORD:
					if (!is_string($value) OR bccomp("18446744073709551616", (string) $value) !== 1 OR bccomp((string) $value, "0") !== 1) {
						throw new Exception("Value is not a string!. ".bccomp("18446744073709551615", (string) $value), self::RULE_VALUE_INVALID);
						}
					break;
				case self::REG_MULTI_SZ:
					if (!is_array($value)) {
						throw new Exception("Value is not an array!", self::RULE_VALUE_INVALID);
						}
					$thisclass = __CLASS__;
					array_walk($value, function($val, $key) use ($thisclass) {if (!is_string($val)) {
						throw new Exception("Array value is not a string!", $thisclass::RULE_VALUE_INVALID);
						}});
					break;
				default:
				}
			}
		$success = self::$Com->$func_array[$value_type](constant("self::$root"), $key_path, $value_name, $value);
		if ($success > 0) {
			throw new Exception("Cannot set value.  Error code $success returned!", self::RULE_OPERATION_FAILED);
			}
		if ($previous_type) {
			self::failsafe_logger(1, $root, $key_path, $value_name, $previous_type, $previous_value);
			}
		else {
			self::failsafe_logger(0, $root, $key_path, $value_name, $value_type, $value);
			}
		return true;
		}
	
	static function DeleteValue($root, $key_path, $value_name) {
		$value_type = self::value_check(1, $root, $key_path, $value_name);
		$value = self::GetValue($root, $key_path, $value_name);
		$success = self::$Com->DeleteValue(constant("self::$root"), $key_path, $value_name);
		if ($success > 0) {
			throw new Exception("Cannot delete value.  Error code $success returned!", self::RULE_OPERATION_FAILED);
			}
		try {
			$results = self::value_check(0, $root, $key_path, $value_name);
			if ($results) {
				throw new Exception("Cannot delete value.  Reason unknown!", self::RULE_OPERATION_FAILED);
				}
			}
		catch (Exception $e) {
			if ($e->getCode() !== self::RULE_VALUE_DOES_NOT_EXIST) {throw $e;}
			}
		self::failsafe_logger(1, $root, $key_path, $value_name, $value_type, $value);
		return true;
		}
	
	static function EnumValues($root, $key_path) {
		self::key_check(0, $root, $key_path);
		$object_collection = new VARIANT();
		$object_type_collection = new VARIANT();
		$success = self::$Com->EnumValues(constant("self::$root"), $key_path, $object_collection, $object_type_collection);
		if ($success > 0) {
			throw new Exception("Cannot iterate values.  Error code $success returned!", self::RULE_OPERATION_FAILED);
			}
		$return_keys = CSTM_variant_get_php_value($object_collection);
		if (!is_array($return_keys)) {return array();}
		$return_values = CSTM_variant_get_php_value($object_type_collection);
		if (!is_array($return_values)) {return array();}
		return array_combine($return_keys, $return_values);
		}

	static function CreateKey($root, $key_path) {
		try {
			$existing = self::key_check(0, $root, $key_path);
			if ($existing) {
				throw new Exception("Key already exists!", 0);
				}
			}
		catch (Exception $e) {
			if ($e->getCode() !== self::RULE_KEY_DOES_NOT_EXIST) {throw $e;}
			}
		$parent_path = explode('\\', $key_path);
		$key_name = array_pop($parent_path);
		$parent_path = implode('\\', $parent_path);
		self::key_check(1, $root, $parent_path);
		$success = self::$Com->CreateKey(constant("self::$root"), $key_path);
		if ($success > 0) {
			throw new Exception("Cannot add key.  Error code $success returned!", self::RULE_OPERATION_FAILED);
			}
		self::key_check(0, $root, $key_path);
		self::failsafe_logger(0, $root, $key_path);
		return true;
		}
	
	static function DeleteKey($root, $key_path) {
		self::key_check(1, $root, $key_path);
		if ((bool) self::EnumKey($root, $key_path)) {
			throw new Exception("Key has subkeys.  Cannot delete until they are removed!", self::RULE_OPERATION_FAILED);
			}
		$contents = self::EnumValues($root, $key_path);
		foreach ($contents as $value_name => $value_type) {
			$value = self::GetValue($root, $key_path, $value_name);
			self::failsafe_logger(1, $root, $key_path, $value_name, $value_type, $value);
			}
		$success = self::$Com->DeleteKey(constant("self::$root"), $key_path);
		if ($success > 0) {
			throw new Exception("Cannot delete value.  Error code $success returned!", self::RULE_OPERATION_FAILED);
			}
		try {
			$results = self::key_check(0, $root, $key_path);
			if ($results) {
				throw new Exception("Cannot delete key.  Reason unknown!", self::RULE_OPERATION_FAILED);
				}
			}
		catch (Exception $e) {
			if ($e->getCode() !== self::RULE_KEY_DOES_NOT_EXIST) {throw $e;}
			}
		self::failsafe_logger(1, $root, $key_path);
		return true;
		}

	static function EnumKey($root, $key_path) {
		self::key_check(0, $root, $key_path);
		$object_collection = new VARIANT();
		$success = self::$Com->EnumKey(constant("self::$root"), $key_path, $object_collection);
		if ($success > 0) {
			throw new Exception("Cannot iterate keys.  Error code $success returned!", self::RULE_OPERATION_FAILED);
			}
		return CSTM_variant_get_php_value($object_collection);
		}

	private static function failsafe_logger($action, $root, $key_path, $value_name=null, $value_type=null, $value=null) {
		// $action = 0=new,1=delete or overwrite
		if ($value_type !== null) {
			$full_key_path = "[".$root.(($key_path) ? '\\'.$key_path : '')."]";
			if ($action === 1) {
				switch ((int) $value_type) {
					case self::REG_SZ:
						$value_converted = '"'.(string) $value.'"';
						break;
					case self::REG_EXPAND_SZ:
						$to_pack = $value."\0";
						$to_pack = mb_convert_encoding($to_pack, "UTF-16LE");
						$value_converted = bin2hex($to_pack);
						$value_converted = str_split($value_converted, 2);
						$value_converted = implode(",", $value_converted);
						break;
					case self::REG_BINARY:
						foreach ($value as $key => &$itm) {
							$itm = sprintf("%02X", $itm);
							}
						$value_converted = implode(",", $value);
						break;
					case self::REG_DWORD:
						$value_converted = sprintf("%08X", $value);
						break;
					case self::REG_QWORD:
						$value_converted = "";
						$quote = $value;
						do {
							$mod = bcmod($quote, "16");
							$quote = bcdiv($quote, "16");
							$value_converted .= dechex($mod);
							} while ($quote > 0);
						$value_converted = strrev($value_converted);
						$value_converted = str_split($value_converted, 2);
						$value_converted = array_reverse($value_converted);
						$value_converted = implode(",", $value_converted);
						break;
					case self::REG_MULTI_SZ:
						$to_pack = implode("\0", $value)."\0\0";
						$to_pack = mb_convert_encoding($to_pack, "UTF-16LE");
						$value_converted = bin2hex($to_pack);
						$value_converted = str_split($value_converted, 2);
						$value_converted = implode(",", $value_converted);
						break;
					default:
						break;
				
					}
				$reg_types = array(
					self::REG_SZ => '',
					self::REG_EXPAND_SZ => 'hex(2):',
					self::REG_BINARY => 'hex:',
					self::REG_DWORD => 'dword:',
					self::REG_QWORD => 'hex(b):',
					self::REG_MULTI_SZ => 'hex(7):'
					);
				$value_name_entry = '"'.$value_name.'"='.$reg_types[$value_type].$value_converted;
				}
			else {
				$value_name_entry = '"'.$value_name.'"=-';
				}
			}
		else {
			$full_key_path = "[".(($action) ? "" : "-").$root.(($key_path) ? '\\'.$key_path : '')."]";
			$value_name_entry = "";
			}
		if (!file_exists(__DIR__.'\\'.self::$reg_rollback_file)) {
			touch(__DIR__.'\\'.self::$reg_rollback_file);
			}
		$header = "Windows Registry Editor Version 5.00";
		$file = trim(file_get_contents(__DIR__.'\\'.self::$reg_rollback_file));
		if (!$file || $file == $header) {$file = "$header\n\n";}
		$file_array = explode("\n", $file);
		array_splice($file_array, 2, 0, array($full_key_path, $value_name_entry, ""));
		$file = implode("\n", $file_array);
		$data = trim($file)."\n\n";
		file_put_contents(__DIR__.'\\'.self::$reg_rollback_file, $data, LOCK_EX);
		echo $data;
		}
	}

?>